﻿using System;
using System.Collections.Generic;

namespace Fuse8.DomainFramework.Common.Utilities
{    
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="TKey"></typeparam>
    /// <typeparam name="TVal"></typeparam>
    public class LRUCacheBag<TKey, TVal> where TVal : class
    {
        #region Fields

        private readonly int _maxItemsCount;
        private readonly Dictionary<TKey, LRUCacheNode<TKey, TVal>> _mainDictionary = new Dictionary<TKey, LRUCacheNode<TKey, TVal>>();

        private LRUCacheNode<TKey, TVal> _headNode;
        private LRUCacheNode<TKey, TVal> _tailNode;
        private LRUCacheNode<TKey, TVal> _currentNode;

        #endregion

        /// <summary>
        /// 
        /// </summary>
        public LRUCacheBag()
            : this(500)
        { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="size"></param>
        public LRUCacheBag(int size)
        {
            _maxItemsCount = size;
        }

        #region Indexer

        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public TVal this[TKey key]
        {
            get
            {
                _currentNode = _mainDictionary[key];

                if (_currentNode == _headNode)
                {
                    //do nothing
                }
                else if (_currentNode == _tailNode)
                {
                    _tailNode = _currentNode.Next;
                    _tailNode.Prev = null;

                    _headNode.Next = _currentNode;
                    _currentNode.Prev = _headNode;
                    _currentNode.Next = null;
                    _headNode = _currentNode;
                }
                else
                {
                    _currentNode.Prev.Next = _currentNode.Next;
                    _currentNode.Next.Prev = _currentNode.Prev;

                    _headNode.Next = _currentNode;
                    _currentNode.Prev = _headNode;
                    _currentNode.Next = null;
                    _headNode = _currentNode;
                }

                return _currentNode.Value;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Add(TKey key, TVal value)
        {
            if (_mainDictionary.Count >= _maxItemsCount)
            {
                //remove old
                _mainDictionary.Remove(_tailNode.Key);

                //reuse old
                LRUCacheNode<TKey, TVal> newNode = _tailNode;
                newNode.Key = key;
                newNode.Value = value;

                _tailNode = _tailNode.Next;
                _tailNode.Prev = null;

                //add new
                _headNode.Next = newNode;
                newNode.Prev = _headNode;
                newNode.Next = null;
                _headNode = newNode;

                _mainDictionary.Add(key, newNode);
            }
            else
            {
                LRUCacheNode<TKey, TVal> newNode = new LRUCacheNode<TKey, TVal>(key, value);

                if (_headNode == null)
                {
                    _headNode = newNode;
                    _tailNode = newNode;
                }
                else
                {
                    _headNode.Next = newNode;
                    newNode.Prev = _headNode;
                    _headNode = newNode;
                }

                _mainDictionary.Add(key, newNode);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public bool TryGetValue(TKey key, out TVal val)
        {
            bool result = Contains(key);

            val = result ? this[key] : default(TVal);

            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool Contains(TKey key)
        {
            return _mainDictionary.ContainsKey(key);
        }

        #endregion
    }

    internal class LRUCacheNode<TKey, TVal>
    {
        #region Properties

        public TKey Key { get; set; }

        public TVal Value { get; set; }

        public LRUCacheNode<TKey, TVal> Next { get; set; }

        public LRUCacheNode<TKey, TVal> Prev { get; set; }

        #endregion

        #region Constructor
        
        public LRUCacheNode(TKey key, TVal val)
        {
            Key = key;
            Value = val;
        }

        #endregion
    }
}
